- 看日常分享: AwesomeCS FB
- 看技術文章: AwesomeCS Wiki
筆者最近在閱讀 aaaddress1 的大作: Windows APT Warfare:惡意程式前線戰術指南,因為書中圍繞著 PE File 進行,腦容量太小的我又一直忘記 PE 的檔案結構,最後決定還是認真的把它研究一遍並寫成筆記。
PE (Portable Executable) 是一種用於可執行文件、目標文件和動態連結庫的文件格式,主要使用在 32 位和 64 位的 Windows 作業系統上。
有點像是 Linux 作業系統中的 elf 檔。
在一串連續的記憶體中, DOS Header 一定會是記憶體中的首段內容, DOS Header 中的幾項資訊會是比較重要的:
e_magic
e_magic 幫助我們辨認該 PE 檔案是否合法,一般來說,它應該永遠等於 MZ 字串。
如果以 C/C++ 檢查 PE File ,可以這樣做:
#include <windows.h>
// ...
void parser(char* filePtr){
IMAGE_DOS_HEADER* dosHdr = (IMAGE_DOS_HEADER *)filePtr;
if(dosHdr->e_magic != IMAGE_DOS_SIGNATURE){
return;
}
// ...
}
// ...
e_lfanew
觀察 e_lfanew 之前,必須先了解什麼是 RVA (Relative Virtual Address), RVA 是程式入口點的參考位址,舉例來說:
如果程式被放入虛擬地址(Virtual address, VA)的 0x01000000
處,且 RVA 位於 0x102D6C
處,那麼程式在記憶體中的實際入口就會是 VA + RVA:
0x01000000
+ 0x00102D6C
= 0x01102D6C
e_lfanew 其實就是指向了 NT Headers 的 RVA ,換個角度思考,我們將前面的 dosHdr 加上偏移量 (在這邊指 RVA),就可以獲得 NT Headers 的起始位址:
IMAGE_NT_HEADERS* ntHdrs = (IMAGE_NT_HEADERS *)((size_t)dosHdr + dosHdr->e_lfanew);
透過讀取 DOS Header 獲得 NT Headers 的起始位址以後,我們就可以對 PE 檔案做更進一步的檢驗。
NT Headers 共包含了兩大結構,分別是 File Header 以及 Optional Header 。
參考上圖,在 File Header 結構中有多個屬性,每個屬性代表的資訊如下:
Machine
紀錄 PE 檔案所存放的機械碼屬於哪一種指令集架構:
NumberOfSections
一個 PE File 通常會有好幾段塊狀區域, NumberofSections 紀錄了 PE 檔案的區段數量。
這個參數對我們撰寫程式解析 PE File 非常有幫助,至於那些塊狀區段存了什麼,晚點會提到。
TimeDateStamp
紀錄程式編譯時間的時間戳。
PointerToSymbolTable
符號表地址,用於除錯,一般為 0 。
NumberOfSymbols
如果符號表存在,這邊會記錄符號數量。
Characteristics
紀錄了整個 PE 的屬性,包含:
Optional Header 的中文稱可選段,實際上,如果要讓 PE 能夠順利地被執行程式裝載器使用, Optional Header 為必備的。
補充:
Optional Header 不存在於 Object File (COFF),而是在編譯的連結階段才會由連結器補上。
參考上圖, Optional Header 包含了很多參數,下面針對重要的參數作介紹:
Address of entry point
程式碼編譯後,程式的入口點,也就代表當 Program 被作業系統載入時, Process 會從這邊開始執行。
一般來說,入口點會指向 .text section 的函式開頭。
ImageBase
記錄了 PE 檔案 mapping 到記憶體上的預設位址,通常為 0x400000
或是 0x800000
。
SizeOfImage
記錄了當程式處於動態執行階段需要多少空間才能存放整個 Image 。
Section alignment
動態的區域對齊, 32-bit 的環境下預設大小為 0x1000 bytes 。
File alignment
靜態的區域對齊, 32-bit 的環境下預設大小為 0x200 bytes 。
假設有不足 0x200 bytes 的資料要放進塊狀區段,塊狀區段的大小為 0x200 bytes ,如果資料多於預設大小,塊狀區段的大小則為 0x400 bytes 。
Size of headers
DOS Header + NT Headers + Section Headers 的大小。
Data directory
Section Headers 的位址緊隨在 NT Headers 的後方,使用 C/C++ 可以輕鬆的獲得其位址:
IMAGE_SECTION_HEADER* sectHdr = (IMAGE_SECTION_HEADER *)((size_t)ntHdrs + sizeof(*ntHdrs));
至於 Section Headers 的本體到底是什麼呢?它其實就是一個存放塊狀區段資訊的陣列:
for (size_t i = 0; i < ntHdrs->FileHeader.NumberOfSections; i++){
printf("\t#%.2x - %8s - %.8x - %.8x \n", i, sectHdr[i].Name, sectHdr[i].PointerToRawData, sectHdr[i].SizeOfRawData);
}
每一塊存放塊狀區段資訊的空間都會有以下屬性:
PointerToRawData
該區段處存在靜態檔案的偏移量。
SizeofRawData
該區段的實際大小。
VirtualAddress
相較於映像基址的相對偏移量。
VirtualSize
顯示該區段需要被分配多少動態空間。
Characteristics
紀錄該區段是否可讀、可寫、可執行。
.text
用於存放程式碼。
.data
用來宣告已初始化的資料與常數。
.bss
存放已宣告但尚未初始化的變數。
.rdata
存放唯讀資料。
.idata
存放引入的函式與資料,這些資料會在 Process 建立時,由執行程式裝載器負責填充。
.edata
存放用來導出給其他程式使用的函式與資料。
.rsrc
用於記錄程式使用了哪些資源。
reloc
重定位,當 PE 程式載入失敗,會以此段作為參考進行調整。
了解 PE File 的基本結構後,我們就可以將惡意 shellcode 添加至目標檔案再將 Entry point 指向惡意程式區段的 Virtual Address 做些壞壞的事(?)
本文章介紹的內容大概只有 Windows APT Warfare:惡意程式前線戰術指南整本書的皮毛,如果想學更多就去下單買一本 Windows APT Warfare:惡意程式前線戰術指南吧!